package com.sk.ccflower.lib.util;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.sk.ccflower.lib.util.reflect.PropertyFilter;

/**
 * Reflection과 프로퍼티 조회 관련 유틸리티 메소드 모음.
 * @author dagui
 *
 */
public class ReflectUtil {
	
	private static final Log log = LogFactory.getLog(ReflectUtil.class);

	/**
	 * 지정한 클래스의 getter 메소드를 포함하는 모든 프로퍼티의 {@link PropertyDescriptor}를 리턴한다.
	 * @param clazz 클래스
	 */
	public static PropertyDescriptor[] getReadableProperties(Class<?> clazz) {
		return getReadableProperties(clazz, null);
	}

	/**
	 * 지정한 클래스의 setter 메소드를 포함하는 모든 프로퍼티의 {@link PropertyDescriptor}를 리턴한다.
	 * @param clazz 클래스
	 */
	public static PropertyDescriptor[] getWritableProperties(Class<?> clazz) {
		return getWritableProperties(clazz, null);
	}

	/**
	 * 지정한 클래스의 getter 메소드를 포함하는 프로퍼티중 지정한
	 * 필터를 충족하는 프로퍼티의 {@link PropertyDescriptor}를 리턴한다.
	 * @param clazz 클래스
	 * @param filter 프로퍼티를 선택할때 사용할 필터
	 * @see PropertyFilter
	 */
	public static PropertyDescriptor[] getReadableProperties(Class<?> clazz, PropertyFilter filter) {
		return getReadableProperties(clazz, filter, false);
	}

	/**
	 * 지정한 클래스의 getter 메소드를 포함하는 프로퍼티중 지정한
	 * 필터를 충족하는 프로퍼티의 {@link PropertyDescriptor}를 리턴한다.
	 * @param clazz 클래스
	 * @param filter 프로퍼티를 선택할때 사용할 필터
	 * @param ifEmptyThenAll 필터 결과가 하나도 없으면 전체를 리턴한다.
	 * @see PropertyFilter
	 */
	public static PropertyDescriptor[] getReadableProperties(Class<?> clazz, PropertyFilter filter, boolean ifEmptyThenAll) {
		PropertyDescriptor[] pds = PropertyUtils.getPropertyDescriptors(clazz);
		List<PropertyDescriptor> all = new ArrayList<PropertyDescriptor>(pds.length);
		List<PropertyDescriptor> filtered = new ArrayList<PropertyDescriptor>(pds.length);

		for (PropertyDescriptor pd: pds) {
			Method getter = pd.getReadMethod();
			if (getter == null
					|| pd.getName().equals("class")
					|| getter.getParameterTypes().length > 0) {
//				if (log.isDebugEnabled()) {
//					log.debug("property " + pd.getName() + " rejected by default policy.");
//				}
				continue;
			}
			all.add(pd);

			if (filter != null && !filter.accept(pd)) {
//				if (log.isDebugEnabled()) {
//					log.debug("property " + pd.getName() + " rejected by filter.");
//				}
				continue;
			}

			filtered.add(pd);
		}
		
		if (filtered.size() == 0) {
			// 필터결과가 하나도 없는 경우
			return (ifEmptyThenAll)? all.toArray(new PropertyDescriptor[]{}): new PropertyDescriptor[]{};
		}
		else {
			return filtered.toArray(new PropertyDescriptor[]{});
		}
	}

	/**
	 * 지정한 클래스의 setter 메소드를 포함하는 프로퍼티중 지정한
	 * 필터를 충족하는 프로퍼티의 {@link PropertyDescriptor}를 리턴한다.
	 * @param clazz 클래스
	 * @param filter 프로퍼티를 선택할때 사용할 필터
	 * @see PropertyFilter
	 */
	public static PropertyDescriptor[] getWritableProperties(Class<?> clazz, PropertyFilter filter) {
		PropertyDescriptor[] pds = PropertyUtils.getPropertyDescriptors(clazz);
		List<PropertyDescriptor> list = new ArrayList<PropertyDescriptor>(pds.length);

		for (PropertyDescriptor pd: pds) {
			Method setter = pd.getWriteMethod();
			if (setter == null
					|| setter.getParameterTypes().length != 1) {
				if (log.isDebugEnabled()) {
					log.debug("property " + pd.getName() + " rejected by default policy.");
				}
				continue;
			}

			if (filter != null && !filter.accept(pd)) {
				if (log.isDebugEnabled()) {
					log.debug("property " + pd.getName() + " rejected by filter.");
				}
				continue;
			}

			list.add(pd);
		}

		return list.toArray(new PropertyDescriptor[]{});
	}

	/*
	 * string manipulations
	 */
	
	/**
	 * 메소드의 전체(modifier, className, throws 를 모두 포함) 시그니쳐를 생성한다.
	 * @see #getMethodSignature(Method, boolean, boolean, boolean)
	 */
	public static String getFullMethodSignature(Method method) {
		return getMethodSignature(method, true, true, true);
	}

	/**
	 * 메소드의 간단한(modifier, className, throws 를 제외) 시그니쳐를 생성한다.
	 * @see #getMethodSignature(Method, boolean, boolean, boolean)
	 */
	public static String getMethodSignature(Method method) {
		return getMethodSignature(method, false, false, false);
	}

	/**
	 * 메소드의 시그니쳐를 생성한다.
	 * @param method 메소드 객체
	 * @param includeModifier modifier 를 포함시킨다.
	 * @param includeClassName 클래스명을 포함시킨다.
	 * @param includeThrows throws 절을 포함시킨다.
	 */
	public static String getMethodSignature(Method method,
			boolean includeModifier, boolean includeClassName,
			boolean includeThrows) {
		StringBuilder b = new StringBuilder();
		
		// 접근자
		if (includeModifier) {
			b.append(Modifier.toString(method.getModifiers())).append(" ");
		}

		// 리턴형
		b.append(method.getReturnType().getCanonicalName()).append(" ");
		
		// 이름
		if (includeClassName)
			b.append(method.getDeclaringClass().getCanonicalName()).append(".");
		b.append(method.getName());

		// 인자 목록
		b.append("(");
		Class<?>[] parameterTypes = method.getParameterTypes();
		for (int i = 0; i < parameterTypes.length; i++) {
			Class<?> parameterType = parameterTypes[i];
			b.append(parameterType.getCanonicalName());
			if (i < parameterTypes.length - 1)
				b.append(", ");
		}
		b.append(")");
		
		// throws
		if (includeThrows) {
			Class<?>[] exceptionTypes = method.getExceptionTypes();
			if (exceptionTypes != null && exceptionTypes.length > 0) {
				b.append(" throws ");
				for (int i = 0; i < exceptionTypes.length; i++) {
					Class<?> exceptionType = exceptionTypes[i];
					b.append(exceptionType.getCanonicalName());
					if (i < exceptionTypes.length - 1)
						b.append(", ");
				}
			}
		}

		return b.toString();
	}

	/**
	 * <p>프로퍼티명에 해당하는 메소드 명을 만들기 위한 이름 변환을 수행한다.</p>
	 * <p>예를들어 propertyName을 입력하면 ProperyName 을 리턴한다.
	 * 리턴된 값에 'get', 'is', 'set'을 붙여주면 메소드명이 된다.</p>
	 * @param propertyName 프로퍼티명
	 * @return 메소드명용 이름
	 */
	public static String getMethodBaseByProperty(String propertyName) {
		// someProperty 같은 경우, 첫 글자를 대문자로 바꾼다.
		if (Character.isLowerCase(propertyName.charAt(0)) &&
				Character.isLowerCase(propertyName.charAt(1))) {
			return StringUtils.capitalize(propertyName);
		}
		// aFlag, AFlag, Aflag 같은 경우, 그대로 리턴한다.
		else {
			return propertyName;
		}
	}
	
	/**
	 * <p>메소드명에 해당하는 프로퍼티 명을 리턴한다.</p>
	 * <p>예를들어 setPropertyName을 입력하면 properyName 을 리턴한다.</p>
	 * @param methodName 메소드명
	 * @return 프로퍼티명
	 */
	public static String getPropertyNameByMethod(String methodName) {
		String name = null;
		if (methodName.startsWith("get") || methodName.startsWith("set")) {
			name = methodName.substring(3);
		}
		else if (methodName.startsWith("is")) {
			name = methodName.substring(2);
		}
		else {
			return null;
		}

		// getSomeProperty 같은 경우, 첫 글자를 대문자로 바꾼다.
		if (Character.isUpperCase(name.charAt(0)) &&
				Character.isLowerCase(name.charAt(1))) {
			return StringUtils.uncapitalize(name);
		}
		// getaFlag, getAFlag, getaflag 같은 경우, 그대로 리턴한다.
		else {
			return name;
		}
	}

	@SuppressWarnings("unchecked")
	public static <T> List<T> cloneList(List<T> list) {
		// 먼저 clone()을 시도한다.
		if (list instanceof Cloneable) {
			try {
				Method cloneMethod = list.getClass().getMethod("clone", new Class<?>[]{});
				if (Modifier.isPublic(cloneMethod.getModifiers()))
					return (List<T>)cloneMethod.invoke(list);
			}
			catch (Exception e) { }
		}

		// 안되면 그냥 복사
		List<T> cloned = new ArrayList<T>();
		cloned.addAll(list);
		return cloned;
	}

}
